4 * @author Ryan Kaldari, 2010
5 * @author Neil Kandalgaonkar, 2010-11
6 * @author Moriel Schottlender, 2015
11 * This is a way of getting simple feedback from users. It's useful
12 * for testing new features -- users can give you feedback without
13 * the difficulty of opening a whole new talk page. For this reason,
14 * it also tends to collect a wider range of both positive and negative
15 * comments. However you do need to tend to the feedback page. It will
16 * get long relatively quickly, and you often get multiple messages
17 * reporting the same issue.
19 * It takes the form of thing on your page which, when clicked, opens a small
20 * dialog box. Submitting that dialog box appends its contents to a
21 * wiki page that you specify, as a new section.
23 * This feature works with any content model that defines a
24 * `mw.messagePoster.MessagePoster`.
26 * Minimal usage example:
28 * var feedback = new mw.Feedback();
29 * $( '#myButton' ).click( function () { feedback.launch(); } );
31 * You can also launch the feedback form with a prefilled subject and body.
32 * See the docs for the #launch() method.
36 * @param {Object} [config] Configuration object
37 * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
39 * @cfg {string} [apiUrl] api.php URL if the feedback page is on another wiki
40 * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the
41 * title of the dialog box
42 * @cfg {mw.Uri|string} [bugsLink="//phabricator.wikimedia.org/maniphest/task/edit/form/1/"] URL where
44 * @cfg {mw.Uri|string} [bugsListLink="//phabricator.wikimedia.org/maniphest/query/advanced"] URL
45 * where bugs can be listed
46 * @cfg {boolean} [showUseragentCheckbox=false] Show a Useragent agreement checkbox as part of the form.
47 * @cfg {boolean} [useragentCheckboxMandatory=false] Make the Useragent checkbox mandatory.
48 * @cfg {string|jQuery} [useragentCheckboxMessage] Supply a custom message for the useragent checkbox.
49 * defaults to the message 'feedback-terms'.
51 mw
.Feedback
= function MwFeedback( config
) {
52 config
= config
|| {};
54 this.dialogTitleMessageKey
= config
.dialogTitleMessageKey
|| 'feedback-dialog-title';
56 // Feedback page title
57 this.feedbackPageTitle
= config
.title
|| new mw
.Title( 'Feedback' );
59 this.messagePosterPromise
= mw
.messagePoster
.factory
.create( this.feedbackPageTitle
, config
.apiUrl
);
60 this.foreignApi
= config
.apiUrl
? new mw
.ForeignApi( config
.apiUrl
) : null;
63 this.bugsTaskSubmissionLink
= config
.bugsLink
|| '//phabricator.wikimedia.org/maniphest/task/edit/form/1/';
64 this.bugsTaskListLink
= config
.bugsListLink
|| '//phabricator.wikimedia.org/maniphest/query/advanced';
67 this.useragentCheckboxShow
= !!config
.showUseragentCheckbox
;
68 this.useragentCheckboxMandatory
= !!config
.useragentCheckboxMandatory
;
69 this.useragentCheckboxMessage
= config
.useragentCheckboxMessage
||
70 $( '<p>' ).append( mw
.msg( 'feedback-terms' ) );
73 this.thankYouDialog
= new OO
.ui
.MessageDialog();
77 OO
.initClass( mw
.Feedback
);
79 /* Static Properties */
80 mw
.Feedback
.static.windowManager
= null;
81 mw
.Feedback
.static.dialog
= null;
86 * Respond to dialog submit event. If the information was
87 * submitted successfully, open a MessageDialog to thank the user.
89 * @param {string} status A status of the end of operation
90 * of the main feedback dialog. Empty if the dialog was
91 * dismissed with no action or the user followed the button
92 * to the external task reporting site.
93 * @param {string} feedbackPageName
94 * @param {string} feedbackPageUrl
96 mw
.Feedback
.prototype.onDialogSubmit = function ( status
, feedbackPageName
, feedbackPageUrl
) {
99 if ( status
!== 'submitted' ) {
104 title
: mw
.msg( 'feedback-thanks-title' ),
105 message
: $( '<span>' ).msg(
110 href
: feedbackPageUrl
116 label
: mw
.msg( 'feedback-close' ),
122 // Show the message dialog
123 this.constructor.static.windowManager
.openWindow(
130 * Modify the display form, and then open it, focusing interface on the subject.
132 * @param {Object} [contents] Prefilled contents for the feedback form.
133 * @param {string} [contents.subject] The subject of the feedback, as plaintext
134 * @param {string} [contents.message] The content of the feedback, as wikitext
136 mw
.Feedback
.prototype.launch = function ( contents
) {
138 if ( !this.constructor.static.dialog
) {
139 this.constructor.static.dialog
= new mw
.Feedback
.Dialog();
140 this.constructor.static.dialog
.connect( this, { submit
: 'onDialogSubmit' } );
142 if ( !this.constructor.static.windowManager
) {
143 this.constructor.static.windowManager
= new OO
.ui
.WindowManager();
144 this.constructor.static.windowManager
.addWindows( [
145 this.constructor.static.dialog
,
149 .append( this.constructor.static.windowManager
.$element
);
152 this.constructor.static.windowManager
.openWindow(
153 this.constructor.static.dialog
,
155 title
: mw
.msg( this.dialogTitleMessageKey
),
156 foreignApi
: this.foreignApi
,
158 messagePosterPromise
: this.messagePosterPromise
,
159 title
: this.feedbackPageTitle
,
160 dialogTitleMessageKey
: this.dialogTitleMessageKey
,
161 bugsTaskSubmissionLink
: this.bugsTaskSubmissionLink
,
162 bugsTaskListLink
: this.bugsTaskListLink
,
164 show
: this.useragentCheckboxShow
,
165 mandatory
: this.useragentCheckboxMandatory
,
166 message
: this.useragentCheckboxMessage
178 * @extends OO.ui.ProcessDialog
181 * @param {Object} config Configuration object
183 mw
.Feedback
.Dialog
= function mwFeedbackDialog( config
) {
184 // Parent constructor
185 mw
.Feedback
.Dialog
.parent
.call( this, config
);
188 this.feedbackPageTitle
= null;
190 this.$element
.addClass( 'mwFeedback-Dialog' );
193 OO
.inheritClass( mw
.Feedback
.Dialog
, OO
.ui
.ProcessDialog
);
195 /* Static properties */
196 mw
.Feedback
.Dialog
.static.name
= 'mwFeedbackDialog';
197 mw
.Feedback
.Dialog
.static.title
= mw
.msg( 'feedback-dialog-title' );
198 mw
.Feedback
.Dialog
.static.size
= 'medium';
199 mw
.Feedback
.Dialog
.static.actions
= [
202 label
: mw
.msg( 'feedback-submit' ),
203 flags
: [ 'primary', 'progressive' ]
207 label
: mw
.msg( 'feedback-external-bug-report-button' ),
212 label
: mw
.msg( 'feedback-cancel' ),
220 mw
.Feedback
.Dialog
.prototype.initialize = function () {
221 var feedbackSubjectFieldLayout
, feedbackMessageFieldLayout
,
222 feedbackFieldsetLayout
, termsOfUseLabel
;
225 mw
.Feedback
.Dialog
.parent
.prototype.initialize
.call( this );
227 this.feedbackPanel
= new OO
.ui
.PanelLayout( {
234 this.feedbackMessageLabel
= new OO
.ui
.LabelWidget( {
235 classes
: [ 'mw-feedbackDialog-welcome-message' ]
237 this.feedbackSubjectInput
= new OO
.ui
.TextInputWidget( {
238 indicator
: 'required'
240 this.feedbackMessageInput
= new OO
.ui
.MultilineTextInputWidget( {
243 feedbackSubjectFieldLayout
= new OO
.ui
.FieldLayout( this.feedbackSubjectInput
, {
244 label
: mw
.msg( 'feedback-subject' )
246 feedbackMessageFieldLayout
= new OO
.ui
.FieldLayout( this.feedbackMessageInput
, {
247 label
: mw
.msg( 'feedback-message' )
249 feedbackFieldsetLayout
= new OO
.ui
.FieldsetLayout( {
250 items
: [ feedbackSubjectFieldLayout
, feedbackMessageFieldLayout
],
251 classes
: [ 'mw-feedbackDialog-feedback-form' ]
254 // Useragent terms of use
255 this.useragentCheckbox
= new OO
.ui
.CheckboxInputWidget();
256 this.useragentFieldLayout
= new OO
.ui
.FieldLayout( this.useragentCheckbox
, {
257 classes
: [ 'mw-feedbackDialog-feedback-terms' ],
261 termsOfUseLabel
= new OO
.ui
.LabelWidget( {
262 classes
: [ 'mw-feedbackDialog-feedback-termsofuse' ],
263 label
: $( '<p>' ).append( mw
.msg( 'feedback-termsofuse' ) )
266 this.feedbackPanel
.$element
.append(
267 this.feedbackMessageLabel
.$element
,
268 feedbackFieldsetLayout
.$element
,
269 this.useragentFieldLayout
.$element
,
270 termsOfUseLabel
.$element
274 this.feedbackSubjectInput
.connect( this, { change
: 'validateFeedbackForm' } );
275 this.feedbackMessageInput
.connect( this, { change
: 'validateFeedbackForm' } );
276 this.feedbackMessageInput
.connect( this, { change
: 'updateSize' } );
277 this.useragentCheckbox
.connect( this, { change
: 'validateFeedbackForm' } );
279 this.$body
.append( this.feedbackPanel
.$element
);
283 * Validate the feedback form
285 mw
.Feedback
.Dialog
.prototype.validateFeedbackForm = function () {
288 !this.useragentMandatory
||
289 this.useragentCheckbox
.isSelected()
291 this.feedbackSubjectInput
.getValue()
294 this.actions
.setAbilities( { submit
: isValid
} );
300 mw
.Feedback
.Dialog
.prototype.getBodyHeight = function () {
301 return this.feedbackPanel
.$element
.outerHeight( true );
307 mw
.Feedback
.Dialog
.prototype.getSetupProcess = function ( data
) {
308 return mw
.Feedback
.Dialog
.parent
.prototype.getSetupProcess
.call( this, data
)
310 // Get the URL of the target page, we want to use that in links in the intro
311 // and in the success dialog
313 if ( data
.foreignApi
) {
314 return data
.foreignApi
.get( {
319 titles
: data
.settings
.title
.getPrefixedText()
320 } ).then( function ( data
) {
321 dialog
.feedbackPageUrl
= OO
.getProp( data
, 'query', 'pages', 0, 'canonicalurl' );
324 this.feedbackPageUrl
= data
.settings
.title
.getUrl();
329 settings
= data
.settings
;
330 data
.contents
= data
.contents
|| {};
332 // Prefill subject/message
333 this.feedbackSubjectInput
.setValue( data
.contents
.subject
);
334 this.feedbackMessageInput
.setValue( data
.contents
.message
);
337 this.messagePosterPromise
= settings
.messagePosterPromise
;
338 this.setBugReportLink( settings
.bugsTaskSubmissionLink
);
339 this.feedbackPageTitle
= settings
.title
;
340 this.feedbackPageName
= settings
.title
.getNameText();
342 // Useragent checkbox
343 if ( settings
.useragentCheckbox
.show
) {
344 this.useragentFieldLayout
.setLabel( settings
.useragentCheckbox
.message
);
347 this.useragentMandatory
= settings
.useragentCheckbox
.mandatory
;
348 this.useragentFieldLayout
.toggle( settings
.useragentCheckbox
.show
);
351 .attr( 'href', this.feedbackPageUrl
)
352 .attr( 'target', '_blank' )
353 .text( this.feedbackPageName
);
354 this.feedbackMessageLabel
.setLabel(
355 mw
.message( 'feedback-dialog-intro', $link
).parseDom()
358 this.validateFeedbackForm();
365 mw
.Feedback
.Dialog
.prototype.getReadyProcess = function ( data
) {
366 return mw
.Feedback
.Dialog
.parent
.prototype.getReadyProcess
.call( this, data
)
368 this.feedbackSubjectInput
.focus();
375 mw
.Feedback
.Dialog
.prototype.getActionProcess = function ( action
) {
376 if ( action
=== 'cancel' ) {
377 return new OO
.ui
.Process( function () {
378 this.close( { action
: action
} );
380 } else if ( action
=== 'external' ) {
381 return new OO
.ui
.Process( function () {
382 // Open in a new window
383 window
.open( this.getBugReportLink(), '_blank' );
387 } else if ( action
=== 'submit' ) {
388 return new OO
.ui
.Process( function () {
390 userAgentMessage
= ':' +
392 mw
.msg( 'feedback-useragent' ) +
394 mw
.html
.escape( navigator
.userAgent
) +
396 subject
= this.feedbackSubjectInput
.getValue(),
397 message
= this.feedbackMessageInput
.getValue();
399 // Add user agent if checkbox is selected
400 if ( this.useragentCheckbox
.isSelected() ) {
401 message
= userAgentMessage
+ message
;
405 return this.messagePosterPromise
.then( function ( poster
) {
406 return fb
.postMessage( poster
, subject
, message
);
408 fb
.status
= 'error4';
409 mw
.log
.warn( 'Feedback report failed because MessagePoster could not be fetched' );
410 } ).then( function () {
413 return fb
.getErrorMessage();
417 // Fallback to parent handler
418 return mw
.Feedback
.Dialog
.parent
.prototype.getActionProcess
.call( this, action
);
422 * Returns an error message for the current status.
426 * @return {OO.ui.Error}
428 mw
.Feedback
.Dialog
.prototype.getErrorMessage = function () {
429 // Messages: feedback-error1, feedback-error2, feedback-error3, feedback-error4
430 return new OO
.ui
.Error( mw
.msg( 'feedback-' + this.status
) );
438 * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
439 * @param {string} subject Subject of message
440 * @param {string} message Body of message
441 * @return {jQuery.Promise} Promise representing success of message posting action
443 mw
.Feedback
.Dialog
.prototype.postMessage = function ( poster
, subject
, message
) {
449 ).then( function () {
450 fb
.status
= 'submitted';
451 }, function ( mainCode
, secondaryCode
, details
) {
452 if ( mainCode
=== 'api-fail' ) {
453 if ( secondaryCode
=== 'http' ) {
454 fb
.status
= 'error3';
455 // ajax request failed
456 mw
.log
.warn( 'Feedback report failed with HTTP error: ' + details
.textStatus
);
458 fb
.status
= 'error2';
459 mw
.log
.warn( 'Feedback report failed with API error: ' + secondaryCode
);
462 fb
.status
= 'error1';
470 mw
.Feedback
.Dialog
.prototype.getTeardownProcess = function ( data
) {
471 return mw
.Feedback
.Dialog
.parent
.prototype.getTeardownProcess
.call( this, data
)
472 .first( function () {
473 this.emit( 'submit', this.status
, this.feedbackPageName
, this.feedbackPageUrl
);
476 this.feedbackPageTitle
= null;
477 this.feedbackSubjectInput
.setValue( '' );
478 this.feedbackMessageInput
.setValue( '' );
479 this.useragentCheckbox
.setSelected( false );
484 * Set the bug report link
486 * @param {string} link Link to the external bug report form
488 mw
.Feedback
.Dialog
.prototype.setBugReportLink = function ( link
) {
489 this.bugReportLink
= link
;
493 * Get the bug report link
495 * @return {string} Link to the external bug report form
497 mw
.Feedback
.Dialog
.prototype.getBugReportLink = function () {
498 return this.bugReportLink
;